﻿/*
 *  This file is part of SXAPI.
 *
 *  SXAPI is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, version 3 of the License.

 *  SXAPI is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with SXAPI.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;

namespace StackExchangeApi
{
    /// <summary>
    /// Use this class to build a SearchQuery out of an initial contraint.
    /// </summary>
    public sealed class Search
    {
        /// <summary>
        /// Constructs a SearchQuery.
        /// </summary>
        /// <param name="constraint">Initial constraint that describes the search</param>
        /// <param name="site">Site to run the search on</param>
        /// <returns>A new SearchQuery</returns>
        public static SearchQuery BuildSearch(SearchConstraint constraint)
        {
            return new SearchQuery(new SearchConstraint[] { constraint });
        }

        /// <summary>
        /// Construct a SearchQuery using a custom string.
        ///
        /// The returns SearchQuery cannot be modified, and attempts
        /// to do so will result in error.
        /// </summary>
        /// <param name="searchQuery">a search query, something that would be typed into the search box of a Trilogy Site</param>
        /// <returns>A new SearchQuery</returns>
        public static SearchQuery BuildSearch(String searchQuery)
        {
            return new SearchQuery(new SearchConstraint[] { new CustomSearchConstraint(searchQuery) });
        }
    }

    /// <summary>
    /// Represents a search that can be run against
    /// one of the "Trilogy Sites".
    /// 
    /// Such searches can return Questions, Answers, or a combination
    /// of both.
    /// </summary>
    public sealed class SearchQuery
    {
        private IEnumerable<SearchConstraint> m_constraints;

        /// <summary>
        /// Construct a new SearchQuery
        /// 
        /// If the constraints are incompatible, will result in an exception
        /// </summary>
        /// <param name="constraints">A set of SearchConstraints to apply</param>
        internal SearchQuery(IEnumerable<SearchConstraint> constraints)
        {
            m_constraints = constraints;

            CheckConstraints();
        }

        /// <summary>
        /// Throws an IncompatibleSearchConstraintsException
        /// if the constrains on this SearchQuery can be guaranteed
        /// to return 0 results.
        /// </summary>
        private void CheckConstraints()
        {
            foreach (SearchConstraint constraint in m_constraints)
                foreach (SearchConstraint other in m_constraints)
                    if (!constraint.CompatibleWith(other))
                        throw new IncompatibleSearchConstraintsException(constraint, other);
        }

        /// <summary>
        /// Creates a new SearchQuery which has all of the
        /// search constraints of this SearchQuery, plus one additional
        /// one.
        /// </summary>
        /// <param name="newConstraint">The new constraint to use</param>
        /// <returns>A new SearchQuery as above</returns>
        public SearchQuery AddConstraint(SearchConstraint newConstraint)
        {
            List<SearchConstraint> newConstraints = new List<SearchConstraint>();
            newConstraints.AddRange(m_constraints);
            newConstraints.Add(newConstraint);

            return new SearchQuery(newConstraints);
        }

        /// <summary>
        /// Executes this SearchQuery against the given site.
        /// </summary>
        /// <param name="site">The given site</param>
        /// <returns>The results of the query</returns>
        public ICollection<Post> Execute(StackExchangeSite site)
        {
            return Execute<Post>(site);
        }

        /// <summary>
        /// Executes this SearchQuery against the given site.
        /// 
        /// It is an error for anything but Answers to result from
        /// executing this query.
        /// </summary>
        /// <param name="site">The given site</param>
        /// <returns>The results of the query</returns>
        public ICollection<Answer> ExecuteExpectingAnswers(StackExchangeSite site)
        {
            bool onlyAnswers = false;

            foreach (SearchConstraint constraint in m_constraints)
                onlyAnswers |= constraint is AnswerSearchConstraint;

            if (!onlyAnswers)
                throw new IncompatibleSearchConstraintsException();

            return Execute<Answer>(site);
        }

        /// <summary>
        /// Executes this SearchQuery against the given site.
        /// 
        /// It is an error for anything but Questions to result from
        /// executing this query.
        /// </summary>
        /// <param name="site">The given site</param>
        /// <returns>The results of the query</returns>
        public ICollection<Question> ExecuteExpectingQuestions(StackExchangeSite site)
        {
            bool onlyQuestions = false;

            foreach (SearchConstraint constraint in m_constraints)
                onlyQuestions |= constraint is QuestionSearchConstraint;

            if (!onlyQuestions)
                throw new IncompatibleSearchConstraintsException();

            return Execute<Question>(site);
        }

        /// <summary>
        /// Execute this SearchQuery against the given site.
        /// </summary>
        /// <typeparam name="T">The type of posts to expect from executing this query</typeparam>
        /// <param name="site">The site to search against</param>
        /// <returns>The results of the search</returns>
        internal ICollection<T> Execute<T>(StackExchangeSite site) where T : Post
        {
            DelayedChunkProvider<T> searchProvider =
                delegate(int chunk, out int chunkCount)
                {
                    List<T> posts = new List<T>();

                    String html = ConnectionMediator.MakeRequest(BuildQueryUri(chunk + 1, site));

                    String nextStr = "<span class=\"page-numbers next\"> next";
                    int a = html.LastIndexOf(nextStr, StringComparison.Ordinal);

                    if (a != -1)
                    {

                        String spanStr = "</span>";
                        int b = html.LastIndexOf(spanStr, a, StringComparison.Ordinal);
                        int c = html.LastIndexOf(">", b, StringComparison.Ordinal);
                        c += 1;

                        chunkCount = Int32.Parse(html.Substring(c, b - c), CultureInfo.InvariantCulture);
                    }
                    else
                    {
                        chunkCount = 1;
                    }

                    String splitStr = "<div class=\"question-summary\" id=\"question-summary";

                    String[] parts = html.Split(new String[] { splitStr }, StringSplitOptions.None);

                    for (int i = 1; i < parts.Length; i++)
                    {
                        String part = parts[i];

                        String voteStr = "<span class=\"vote-count-post\"><strong>";
                        int p = part.IndexOf(voteStr, StringComparison.Ordinal);
                        p += voteStr.Length;
                        int q = part.IndexOf("</strong>", p, StringComparison.Ordinal);

                        String votesS = part.Substring(p, q - p);

                        int c = votesS.Length - 1;
                        int d = -1;

                        while (c >= 0)
                        {
                            if (Char.IsDigit(votesS[c])) d = c + 1;

                            if (d != -1 && !Char.IsDigit(votesS[c])) { c++; break; }

                            c--;
                        }

                        if (c < 0) c = 0;

                        votesS = votesS.Substring(c, d - c);

                        long votes = Int64.Parse(votesS, CultureInfo.InvariantCulture);

                        long? views = null;

                        String viewStr = "<div class=\"views\">";

                        int r = part.IndexOf(viewStr, q, StringComparison.Ordinal);
                        int s;
                        if (r != -1)
                        {
                            r += viewStr.Length;
                            s = part.IndexOf(" ", r, StringComparison.Ordinal);

                            String viewsS = part.Substring(r, s - r);

                            long tempViews;

                            if (Int64.TryParse(viewsS, NumberStyles.Integer, CultureInfo.InvariantCulture, out tempViews))
                                views = tempViews;
                        }
                        else
                        {
                            s = q;
                        }

                        String summaryStr = "<div class=\"summary\">";
                        int t = part.IndexOf(summaryStr, s, StringComparison.Ordinal);
                        t += summaryStr.Length;

                        String linkStr = "href=\"";
                        int u = part.IndexOf(linkStr, t, StringComparison.Ordinal);
                        u += linkStr.Length;

                        int v = part.IndexOf("\"", u, StringComparison.Ordinal);

                        String link = part.Substring(u, v - u);

                        Post post = ParsePost(link, views, site);
                        post.Votes = votes;

                        posts.Add((T)post);
                    }

                    return posts;
                };

            return new DelayLoadedList<T>(searchProvider, 50);
        }

        /// <summary>
        /// Given a link to the post, returns either a Question or an Answer
        /// which can query it.
        /// </summary>
        /// <param name="link">The link, as a string</param>
        /// <param name="views">The number of views, if known (null otherwise)</param>
        /// <param name="site">The site the link is on</param>
        /// <returns>A Post which represents the link</returns>
        private static Post ParsePost(String link, long? views, StackExchangeSite site)
        {
            int pound = link.IndexOf("#", StringComparison.Ordinal);

            if (pound == -1)
            {
                String quesStr = "/questions/";
                int p = link.IndexOf(quesStr, StringComparison.Ordinal);
                p += quesStr.Length;
                int q = link.IndexOf("/", p, StringComparison.Ordinal);

                long id = Int64.Parse(link.Substring(p, q - p), CultureInfo.InvariantCulture);

                Question ques = Question.GetQuestionWithId(id, site);
                
                if(views != null)
                    ques.ViewCount = (long)views;

                return ques;
            }

            long aId = Int64.Parse(link.Substring(pound + 1), CultureInfo.InvariantCulture);

            return Answer.GetAnswerWithId(aId, site);
        }

        /// <summary>
        /// Builds a Uri that, when fetched, gets a given page of this SearchQuery's results
        /// on the given site.
        /// </summary>
        /// <param name="page">Page to fetch</param>
        /// <param name="site">Site to query</param>
        /// <returns>A Uri as above</returns>
        private Uri BuildQueryUri(int page, StackExchangeSite site)
        {
            bool allTags = true;

            List<String> queryParts = new List<String>();
            foreach (SearchConstraint part in m_constraints)
            {
                queryParts.Add(part.SearchString());
                allTags &= (part is HasTag);
            }

            

            String uriStr;

            //Do to redirection messing with pagesize
            //  we need to treat a search that is ONLY tag constraints... specially.
            if (allTags)
            {
                for(int i = 0; i < queryParts.Count; i++)
                    queryParts[i] = queryParts[i].Substring(1, queryParts[i].Length - 2);

                String qStr = String.Join(" ", queryParts.ToArray());
                qStr = qStr.Trim();

                uriStr = String.Format(
                    CultureInfo.InvariantCulture,
                    "http://{0}/questions/tagged?tagnames={1}&page={2}&sort=newest&pagesize=50",
                    Utils.GetAddressForSite(site),
                    qStr,
                    page);
            }
            else
            {
                String qStr = String.Join("+", queryParts.ToArray());
                qStr = qStr.Trim();

                uriStr =
                    String.Format(
                      CultureInfo.InvariantCulture,
                      "http://{0}/search?q={1}&page={2}&tab=relevance&pagesize=50",
                      Utils.GetAddressForSite(site),
                      qStr,
                      page);
            }

            return new Uri(uriStr);
        }
    }
}
